﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Drawing;
using System.Windows.Media.Media3D;


namespace PlanetsAndOrbs
{
    /// <summary>
    /// Class which holds the rendered objects and can render a ray.
    /// </summary>
    public class PixelRenderer
    {
        World   world_;

        Vector3D vViewPoint_ = new Vector3D(0, 3, 0);

        List<Object3D> objects_ = new List<Object3D>();
        
        public Vector3D ViewPoint { set { vViewPoint_ = value; } }

        public PixelRenderer()
        {
        }

        /// <summary>
        /// Converts Color to a Vector3D by removing alpha and scaling to 0..1.
        /// </summary>
        /// <param name="c">Color to convert.</param>
        /// <returns>Vector representation of the color.</returns>
        static public Vector3D color2V3D(Color c)
        {
            return new Vector3D((float)c.R / 255, (float)c.G / 255, (float)c.B / 255);
        }

        public void setWorld(Bitmap bmp)
        {
            objects_.Remove(world_);
            world_ = new ImageWorld(bmp);           // world with background image
            objects_.Add(world_);
        }

        public void setWorld(Color sky, Color horizont)
        {
            objects_.Remove(world_);
            world_ = new ColorWorld(sky, horizont); // world with color gradient
            objects_.Add(world_);
        }

        public void regenerateObjects(Random r, int orbs, int planets, int ripples)
        {
            // need these vects to position objects
            Vector3D vForward = new Vector3D(0, 0, 1);
            Vector3D vRight = new Vector3D(1, 0, 0);
            Vector3D vUp = new Vector3D(0, 1, 0);

            objects_.Clear();       // removed all previous objects

            if (world_ == null)     // if the world was not set - create a standard one
                world_ = (World)new ColorWorld(Color.Blue, Color.SandyBrown);
            objects_.Add(world_);

            objects_.Add(new Water( 
                ripples,
                Color.FromArgb(255, r.Next(255), r.Next(255), r.Next(255)),
                Color.FromArgb(255, r.Next(127), r.Next(127), r.Next(127)),
                r));

            // generating reflective and transparent orbs
            for (int i = 0; i < orbs; ++i)
            {
                Vector3D center =
                    vForward * ((r.Next(300) - 50) / 10.0f) +
                    vRight * ((r.Next(300) - 150) / 10.0f) +
                    vUp * ((r.Next(80) - 5) / 10.0f);
                float radius = (r.Next(15) + 5) / 10.0f;
                Object3D o = r.Next(10) > 4
                    ? (Object3D) new ReflectiveSphere(
                            center, radius,
                            Color.FromArgb(255, r.Next(255), r.Next(255), r.Next(255)),
                            Color.FromArgb(255, r.Next(127), r.Next(127), r.Next(127))) 
                    : (Object3D) new TransparentSphere(
                        center, radius, 
                        r.NextDouble(),
                        Color.FromArgb(255, r.Next(255), r.Next(255), r.Next(255)),
                        Color.FromArgb(255, r.Next(255), r.Next(255), r.Next(255)));
                objects_.Add(o);
            }

            if (planets > 0)
            {   // generating planets
                Bitmap moonMapImage = new Bitmap(Resources.moon);
                Bitmap earthMapImage = new Bitmap(Resources.earth);
                Bitmap marsMapImage = new Bitmap(Resources.mars);

                for (int i = 0; i < planets; ++i)
                {
                    Vector3D center =
                        vForward * ((r.Next(300) - 50) / 10.0f) +
                        vRight * ((r.Next(300) - 150) / 10.0f) +
                        vUp * ((r.Next(80) - 5) / 10.0f);

                    Object3D o;
                    switch (r.Next(3)) 
                    {
                        case 0:
                            o = new Planet(center, 3, earthMapImage, r);
                            break;

                        case 1:
                            o = new Planet(center, 0.8, moonMapImage, r);
                            break;

                        default:
                            o = new Planet(center, 1.5, marsMapImage, r);
                            break;

                    }
                    objects_.Add(o);
                }
            }
        }

        /// <summary>
        /// Renders one pixel.
        /// </summary>
        /// <param name="x">x coordinate of the pixel.</param>
        /// <param name="y">y coordinate of the pixel.</param>
        /// <returns>The pixel color.</returns>
        public Color render(double x, double y)
        {
            if (objects_.Count == 0)
                return Color.Red;   // no objects - return red

            // starting from the viewpoint
            Vector3D origin = new Vector3D();
            origin = vViewPoint_;
            
            // making up the ray vector from bitmap coordinates
            double cy = Math.Cos(y);
            Vector3D ray = new Vector3D(cy * Math.Sin(x), -Math.Sin(y), cy * Math.Cos(x));

            // loop through the objects and find the one with the 
            // nearest point of intersection  with the ray vector
            Object3D lastObject = null;
            Vector3D accFilter = new Vector3D(1, 1, 1);
            Vector3D accEmission = new Vector3D(0, 0, 0);
            for (int i = 0; i < 100; ++i)
            {
                double bestDistance = 1e11; // even beyond the world boundary
                Object3D bestObj = null;
                for (List<Object3D>.Enumerator e = objects_.GetEnumerator(); e.MoveNext(); )
                {
                    Object3D obj = e.Current;
                    if (obj == lastObject)
                        continue;   // we do not interact with the last object 

                    double dist = obj.distance(origin, ray);
                    if (dist >= 0 && dist < bestDistance)
                    {
                        bestObj = obj;
                        bestDistance = dist;
                    }
                }

                // we remember the object to avoid sticking to the surface
                lastObject = bestObj;

                // move the origin to the intersection point
                origin += ray * bestDistance;

                // the selected object performs optical transformation of the ray vector 
                // and the color transformation (filtering)
                Vector3D filter, emission;
                bool result = bestObj.trace(ref origin, ref ray, out filter, out emission);

                if (result)
                {   // a non-transparent and non-reflective object - end of tracing, apply the filters
                    Vector3D retVal = new Vector3D(
                        accFilter.X * emission.X,
                        accFilter.Y * emission.Y,
                        accFilter.Z * emission.Z) + accEmission;
                    return Color.FromArgb(0xff,
                        (int)(retVal.X * 255),
                        (int)(retVal.Y * 255),
                        (int)(retVal.Z * 255));
                }
                else
                {   // a transparent or reflective object - tracing will go on, accumulate the filters
                    accEmission += new Vector3D(
                        accFilter.X * emission.X,
                        accFilter.Y * emission.Y,
                        accFilter.Z * emission.Z);

                    accFilter = new Vector3D(
                        accFilter.X * filter.X,
                        accFilter.Y * filter.Y,
                        accFilter.Z * filter.Z);
                }
            }
            return Color.Black; // in case of too many reflections
        }
    }
}
